//**********************************************************************
//
//<copyright>
//
//BBN Technologies
//10 Moulton Street
//Cambridge, MA 02138
//(617) 873-8000
//
//Copyright (C) BBNT Solutions LLC. All rights reserved.
//
//</copyright>
//**********************************************************************
//
//$Source:
///cvs/darwars/ambush/aar/src/com/bbn/ambush/mission/MissionHandler.java,v
//$
//$RCSfile: MissionHandler.java,v $
//$Revision: 1.10 $
//$Date: 2004/10/21 20:08:31 $
//$Author: dietrick $
//
//**********************************************************************

package com.bbn.openmap.omGraphics.util;

import java.awt.Image;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.awt.image.PixelGrabber;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.bbn.openmap.dataAccess.image.WorldFile;
import com.bbn.openmap.omGraphics.OMRaster;
import com.bbn.openmap.proj.Projection;
import com.bbn.openmap.proj.coords.GeoCoordTransformation;
import com.bbn.openmap.proj.coords.LatLonGCT;
import com.bbn.openmap.proj.coords.LatLonPoint;
import com.bbn.openmap.util.DataBounds;
import com.bbn.openmap.util.Debug;

/**
 * The ImageTranslator is the object that takes a BufferedImage and creates an
 * OMRaster from it based on a Projection object.
 */
public class ImageWarp {

    public static Logger logger = Logger.getLogger("com.bbn.openmap.omGraphics.util.ImageWarp");

    /**
     * Source image pixels.
     */
    protected int[] pixels = null;

    /** Image width, */
    protected int iwidth;
    /** Image height, */
    protected int iheight;
    /**
     * Horizontal units/pixel in the source BufferedImage projection. Assumed to
     * be constant across the image.
     */
    protected double hor_upp;
    /**
     * Vertical units/pixel in the source BufferedImage projection. Assumed to
     * be constant across the image.
     */
    protected double ver_upp;
    /**
     * The vertical origin pixel location in the source image for the coordinate
     * system origin.
     */
    protected double verOrigin;
    /**
     * The horizontal origin pixel location in the source image for the
     * coordinate system origin.
     */
    protected double horOrigin;

    /**
     * A transformation for the projection of the source image. If not set, the
     * image is assumed to be equal arc projection.
     */
    protected GeoCoordTransformation geoTrans = new LatLonGCT();

    /**
     * The coordinate bounds of the image, represented in the coordinate system
     * of the image.
     */
    protected DataBounds sourceImageBounds;

    /**
     * The coordinate image bounds of the projected image on the map window.
     */
    protected DataBounds projectedImageBounds;

    /**
     * Create an image warp for an image assumed to be world wide coverage, with
     * the top at 90 degrees, the bottom at -90, the left side at -180 and the
     * right side at 180. Assumes the origin point is in the middle of the
     * image.
     */
    public ImageWarp(BufferedImage bi) {
        this(bi, LatLonGCT.INSTANCE, new DataBounds(-180.0, -90.0, 180.0, 90.0));
    }

    /**
     * Create an image warp with some additional transform information.
     * 
     * @param bi BufferedImage of the source
     * @param transform the GeoCoordTransformation for the projection of the
     *        image.
     * @param imageBounds the bounds of the image in the image's coordinate
     *        system.
     */
    public ImageWarp(BufferedImage bi, GeoCoordTransformation transform, DataBounds imageBounds) {
        if (bi != null) {
            iwidth = bi.getWidth();
            iheight = bi.getHeight();
            setGeoTrans(transform);
            setImageBounds(imageBounds);

            pixels = getPixels(bi, 0, 0, iwidth, iheight);

            // See if this saves on memory. Seems to.
            bi = null;
        }
    }

    /**
     * Create an image warp with some additional transform information.
     * 
     * @param bi BufferedImage of the source
     * @param transform the GeoCoordTransformation for the projection of the
     *        image.
     * @param worldFile the WorldFile describing the image's location.
     */
    public ImageWarp(BufferedImage bi, GeoCoordTransformation transform, WorldFile worldFile) {
        if (bi != null) {
            iwidth = bi.getWidth();
            iheight = bi.getHeight();
            setGeoTrans(transform);

            setImageBounds(worldFile);

            pixels = getPixels(bi, 0, 0, iwidth, iheight);

            // See if this saves on memory. Seems to.
            bi = null;
        }
    }

    /**
     * Create an image warp for an image assumed to be world wide coverage, with
     * the top at 90 degrees, the bottom at -90, the left side at -180 and the
     * right side at 180. Assumes the origin point is in the middle of the
     * image.
     * 
     * @param pix ARGB array of pixel values for image.
     * @param width pixel width of image.
     * @param height pixel height of image.
     */
    public ImageWarp(int[] pix, int width, int height) {
        this(pix, width, height, LatLonGCT.INSTANCE, new DataBounds(-180.0, -90.0, 180.0, 90.0));
    }

    /**
     * Create an image warp with some additional transform information.
     * 
     * @param pix ARGB array of pixel values for image.
     * @param width pixel width of image.
     * @param height pixel height of image.
     * @param transform the GeoCoordTransformation for the projection of the
     *        image.
     * @param imageBounds the bounds of the image in the image's coordinate
     *        system.
     */
    public ImageWarp(int[] pix, int width, int height, GeoCoordTransformation transform,
            DataBounds imageBounds) {
        if (pix != null) {
            iwidth = width;
            iheight = height;
            setGeoTrans(transform);
            setImageBounds(imageBounds);
            pixels = pix;
        }
    }

    /**
     * Create an image warp with some additional transform information.
     * 
     * @param pix ARGB array of pixel values for image.
     * @param width pixel width of image.
     * @param height pixel height of image.
     * @param transform the GeoCoordTransformation for the projection of the
     *        image.
     * @param worldFile the WorldFile describing the image's location.
     */
    public ImageWarp(int[] pix, int width, int height, GeoCoordTransformation transform,
            WorldFile worldFile) {
        if (pix != null) {
            iwidth = width;
            iheight = height;
            setGeoTrans(transform);
            setImageBounds(worldFile);
            pixels = pix;
        }
    }

    /**
     * The pixels used in the OMRaster.
     */
    // int[] tmpPixels = new int[0];
    /**
     * Return an OMRaster that covers the given projection, with the image
     * warped for the projection.
     * 
     * @param p map projection
     * @return OMRaster or null if the image isn't within the current
     *         projection.
     */
    public OMRaster getOMRaster(Projection p) {
        int[] pixels = getImagePixels(p);
        if (pixels != null && projectedImageBounds != null) {
            int width = (int) Math.ceil(projectedImageBounds.getWidth());
            int height = (int) Math.ceil(projectedImageBounds.getHeight());
            int x = (int) Math.floor(projectedImageBounds.getMin().getX());
            int y = (int) Math.floor(projectedImageBounds.getMin().getY());
            OMRaster raster = new OMRaster(x, y, width, height, pixels);
            raster.generate(p);
            return raster;
        }

        return null;
    }

    /**
     * Given a projection, return the pixels for an image that will cover the
     * projection area.
     * 
     * @param p map projection
     * @return int[] of ARGB pixels for an image covering the given projection.
     */
    public int[] getImagePixels(Projection p) {
        if (pixels != null && p != null) {

            projectedImageBounds = calculateProjectedImageBounds(p);

            if (projectedImageBounds == null) {
                // image isn't on the map.
                return null;
            }

            int projHeight = (int) Math.ceil(projectedImageBounds.getHeight());
            int projWidth = (int) Math.ceil(projectedImageBounds.getWidth());

            // See if we can reuse the pixel array we have.

            int[] tmpPixels = new int[projWidth * projHeight];
            int numTmpPixels = tmpPixels.length;
            logger.fine("tmpPixels[" + numTmpPixels + "]");
            int clear = 0x00000000;

            Point2D ctp = new Point2D.Double();
            Point2D ddll = new Point2D.Double();
            Point2D imageCoord = new Point2D.Double();
            Point2D center = p.getCenter();

            if (logger.isLoggable(Level.FINE)) {
                logger.fine(projectedImageBounds.toString());
            }

            int minx = (int) Math.floor(projectedImageBounds.getMin().getX());
            int miny = (int) Math.floor(projectedImageBounds.getMin().getY());
            int maxx = (int) Math.ceil(projectedImageBounds.getMax().getX());
            int maxy = (int) Math.ceil(projectedImageBounds.getMax().getY());

            // i and j are map window pixel values.
            for (int i = minx; i < maxx; i++) {
                for (int j = miny; j < maxy; j++) {

                    // ix and iy are pixel coordinates of the destination image.
                    int ix = i - minx;
                    int iy = j - miny;

                    // index into the OMRaster pixel array
                    int tmpIndex = (ix + (iy * projWidth));

                    if (tmpIndex >= numTmpPixels) {
                        continue;
                    }

                    ddll = p.inverse(i, j, ddll);

                    // If the llp calculated isn't on the map,
                    // don't bother drawing it. Could be a space
                    // point in Orthographic projection, for
                    // instance.
                    if (ddll.equals(center)) {
                        p.forward(ddll, ctp);
                        if (ctp.getX() != i || ctp.getY() != j) {
                            tmpPixels[tmpIndex] = clear;
                            continue;
                        }
                    }

                    if (geoTrans != null) {
                        geoTrans.forward(ddll.getY(), ddll.getX(), imageCoord);
                    } else {
                        imageCoord = ddll;
                    }

                    if (!sourceImageBounds.contains(imageCoord)) {
                        tmpPixels[tmpIndex] = clear;
                        continue;
                    }

                    // Find the corresponding pixel location in
                    // the source image.
                    int horIndex = (int) Math.round(horOrigin + (imageCoord.getX() / hor_upp));
                    int verIndex = (int) Math.round(verOrigin + (imageCoord.getY() / ver_upp));

                    if (horIndex < 0 || horIndex >= iwidth || verIndex < 0 || verIndex >= iheight) {
                        // pixel not on the source image. This
                        // happens if the image doesn't cover the
                        // entire earth.
                        continue;
                    }

                    int imageIndex = horIndex + (verIndex * iwidth);

                    if (imageIndex >= 0 && imageIndex < pixels.length) {
                        tmpPixels[tmpIndex] = pixels[imageIndex];
                    }
                }
            }

            logger.fine("finished creating image");
            return tmpPixels;
        }

        logger.warning("problem creating image, no pixels: " + (pixels == null ? "true" : "false")
                + ", no projection:" + (p == null ? "true" : "false"));

        // If you get here, something's not right.
        return null;
    }

    /**
     * Returns the image bounds of the image as it would be warped to the
     * provided projection.
     * 
     * @param p Projection the image will be displayed on
     * @return DataBounds, in the projected coordinate space.
     */
    public DataBounds calculateProjectedImageBounds(Projection p) {

        // This doesn't seem to do anything but slow things down.
        // if (geoTrans.equals(LatLonGCT.INSTANCE)) {
        // // whole earth
        // logger.fine("just using whole screen image");
        // return new DataBounds(0, 0, p.getWidth(), p.getHeight());
        // }

        DataBounds db = null;
        if (sourceImageBounds != null) {
            int pw = p.getWidth();
            int ph = p.getHeight();
            Point2D min = sourceImageBounds.getMin();
            Point2D max = sourceImageBounds.getMax();
            double x1 = Math.floor(min.getX());
            double y1 = Math.floor(min.getY());
            double x2 = Math.ceil(max.getX());
            double y2 = Math.ceil(max.getY());
            double width = sourceImageBounds.getWidth();
            double height = sourceImageBounds.getHeight();

            // These are just memory savers, reused for every calculation.
            LatLonPoint tmpG = new LatLonPoint.Double();
            Point2D tmpP = new Point2D.Double();

            db = new DataBounds();
            db.setHardLimits(new DataBounds(0, 0, pw, ph));
            db.add(p.forward(geoTrans.inverse(x1, y1, tmpG), tmpP));
            db.add(p.forward(geoTrans.inverse(x1, y2, tmpG), tmpP));
            db.add(p.forward(geoTrans.inverse(x2, y1, tmpG), tmpP));
            db.add(p.forward(geoTrans.inverse(x2, y2, tmpG), tmpP));

            double numSplits = 4;

            double xSpacer = width / numSplits;
            double ySpacer = height / numSplits;

            for (int i = 1; i < numSplits; i++) {
                db.add(p.forward(geoTrans.inverse(Math.ceil(x1 + xSpacer * i), y1, tmpG), tmpP));
                db.add(p.forward(geoTrans.inverse(x1, Math.ceil(y1 + ySpacer * i), tmpG), tmpP));
                db.add(p.forward(geoTrans.inverse(Math.ceil(x1 + xSpacer * i), y2, tmpG), tmpP));
                db.add(p.forward(geoTrans.inverse(x2, Math.ceil(y1 + ySpacer * i), tmpG), tmpP));
            }

            if (db.getWidth() <= 0 || db.getHeight() <= 0) {
                logger.fine("dimensions of data bounds bad, returning null " + db);
                return null;
            }

        }
        return db;

    }

    /**
     * Convenience function to get projected image bounds for an image that has
     * been warped for a given projection. If you've grabbed the pixel ints,
     * this is how you get the projected image bounds for those ints.
     * 
     * @return Rectangle for bounds
     */
    public Rectangle getProjectedImageBoundsForLastProjection() {

        DataBounds projImageBounds = projectedImageBounds;
        Rectangle rect = null;

        if (projImageBounds != null) {
            int minx = (int) Math.floor(projImageBounds.getMin().getX());
            int miny = (int) Math.floor(projImageBounds.getMin().getY());
            int maxx = (int) Math.ceil(projImageBounds.getMax().getX());
            int maxy = (int) Math.ceil(projImageBounds.getMax().getY());

            rect = new Rectangle(minx, miny, maxx - minx, maxy - miny);
        }
        return rect;
    }

    /**
     * Get the pixels from the BufferedImage. If anything goes wrong, returns a
     * int[0].
     */
    protected int[] getPixels(Image img, int x, int y, int w, int h) {
        int[] pixels = new int[w * h];
        PixelGrabber pg = new PixelGrabber(img, x, y, w, h, pixels, 0, w);
        try {
            pg.grabPixels();
        } catch (InterruptedException e) {
            Debug.error("ImageTranslator: interrupted waiting for pixels!");
            return new int[0];
        }

        if ((pg.getStatus() & ImageObserver.ABORT) != 0) {
            System.err.println("ImageTranslator: image fetch aborted or errored");
            return new int[0];
        }

        return pixels;
    }

    public int getIwidth() {
        return iwidth;
    }

    public void setIwidth(int iwidth) {
        this.iwidth = iwidth;
    }

    public int getIheight() {
        return iheight;
    }

    public void setIheight(int iheight) {
        this.iheight = iheight;
    }

    public double getHor_dpp() {
        return hor_upp;
    }

    public void setHor_dpp(double hor_dpp) {
        this.hor_upp = hor_dpp;
    }

    public double getVer_dpp() {
        return ver_upp;
    }

    public void setVer_dpp(double ver_dpp) {
        this.ver_upp = ver_dpp;
    }

    public double getVerOrigin() {
        return verOrigin;
    }

    public void setVerOrigin(double verOrigin) {
        this.verOrigin = verOrigin;
    }

    public double getHorOrigin() {
        return horOrigin;
    }

    public void setHorOrigin(double horOrigin) {
        this.horOrigin = horOrigin;
    }

    public GeoCoordTransformation getGeoTrans() {
        return geoTrans;
    }

    public void setGeoTrans(GeoCoordTransformation geoTrans) {
        this.geoTrans = geoTrans;
    }

    public DataBounds getImageBounds() {
        return sourceImageBounds;
    }

    public void setImageBounds(DataBounds imageBounds) {
        this.sourceImageBounds = imageBounds;

        hor_upp = imageBounds.getWidth() / iwidth;
        // need the negative sign because latitudes increase in the opposite
        // direction as y pixel values.
        boolean yDirUp = imageBounds.isyDirUp();

        ver_upp = imageBounds.getHeight() / iheight;
        if (yDirUp) {
            ver_upp *= -1;
        }

        // We should be able to just go from the lower left corner of the image
        // and find zero from there, the min of both bounds values.

        double leftX = imageBounds.getMin().getX();
        double upperY = yDirUp ? imageBounds.getMax().getY() : imageBounds.getMin().getY();

        verOrigin = -upperY / ver_upp; // number of Y pixels to origin.
        horOrigin = -leftX / hor_upp; // number of X pixels to origin.

        if (logger.isLoggable(Level.FINE)) {
            logger.fine("getting image pixels w:" + iwidth + ", h:" + iheight + ", hor upp:"
                    + hor_upp + ", ver upp:" + ver_upp + ", verOrigin:" + verOrigin
                    + ", horOrigin:" + horOrigin);
            logger.fine(imageBounds.toString());
        }
    }

    public void setImageBounds(WorldFile worldFile) {
        hor_upp = worldFile.getXDim();
        // world file dimensions have direction, negative for going down
        ver_upp = worldFile.getYDim();

        double leftX = worldFile.getX();
        double upperY = worldFile.getY();

        verOrigin = -worldFile.getY() / ver_upp; // number of Y pixels to
        // origin.
        horOrigin = -leftX / hor_upp; // number of X pixels to origin.

        sourceImageBounds = new DataBounds(leftX, worldFile.getY() + ver_upp * iheight, leftX
                + hor_upp * iwidth, upperY);

        if (logger.isLoggable(Level.FINE)) {
            logger.fine("getting image pixels w:" + iwidth + ", h:" + iheight + ", hor upp:"
                    + hor_upp + ", ver upp:" + ver_upp + ", verOrigin:" + verOrigin
                    + ", horOrigin:" + horOrigin);
            logger.fine(sourceImageBounds.toString());
        }
    }

    public static void main(String[] args) {
        new ImageWarp(new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB), LatLonGCT.INSTANCE, new DataBounds(25, -90, 180, 90));
    }
}
